﻿#include "texture.h"
//#include <errno.h>
//#include <opencv2/imgcodecs.hpp>
//#include <opencv2/imgproc.hpp>
#include <glad/glad.h>

#define STB_IMAGE_IMPLEMENTATION
#include "stb/image.h"

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb/image_write.h"

using namespace std;
using namespace anip;

Texture2D::Texture2D(const TextureOptions* options):
	_targetType(GL_TEXTURE_2D)
{
	glGenTextures(1, &_htex) ERROR_CHECKED;
	glBindTexture(GL_TEXTURE_2D, _htex) ERROR_CHECKED;


	processOptions(options, nullptr);
}

Texture2D::Texture2D(int w, int h, const TextureOptions* options):
	_width(w), _height(h), _targetType(GL_TEXTURE_2D)
{
	assert(w > 0 && h > 0);

	glGenTextures(1, &_htex) ERROR_CHECKED;
	glBindTexture(GL_TEXTURE_2D, _htex) ERROR_CHECKED;

	processOptions(options, nullptr);
}

Texture2D* anip::Texture2D::createFromData(
	int w, int h, const void* data, const TextureOptions* options)
{
	assert(w > 0 && h > 0);
	Texture2D* tex = new Texture2D(options);
	tex->_width = w;
	tex->_height = h;
	tex->processOptions(options, data);
	return tex;
}

Texture2D* anip::Texture2D::createFromFile(string path, const TextureOptions* options)
{
	// ANIP_TODO must rewrite this, not very clean now
	Texture2D* tex = new Texture2D(options);

	unsigned char* data = stbi_load(path.c_str(), 
		&tex->_width, &tex->_height, &tex->_channels, 0);
	if (data == nullptr)
	{
		string err = "Error reading image file: "s + stbi_failure_reason()
			+ "\nFilename is " + path;
		stbi_image_free(data);
		tex->finalize();
		delete tex;
		throw runtime_error(err.c_str());
	}

	switch (tex->_channels)
	{
	case 1: tex->_format = ColorFormat::Red; break;
	case 2: 
		stbi_image_free(data);
		tex->finalize();
		delete tex;
		throw runtime_error("Unsupported image format: gray + alpha");
	case 3: tex->_format = ColorFormat::RGB; break;
	case 4: tex->_format = ColorFormat::RGBA; break;
	default: 
		stbi_image_free(data);
		tex->finalize();
		delete tex; 
		throw runtime_error("Unknown image channel format");
	}

	//glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexImage2D(GL_TEXTURE_2D, 0, tex->_internalFormat, tex->_width, tex->_height,
		0, (GLenum)tex->_format, tex->_type, data) ERROR_CHECKED;

	stbi_image_free(data);
	return tex;

}

Texture2D* anip::Texture2D::createMultisample(int w, int h, int samples)
{
	assert(w > 0 && h > 0);

	Texture2D* tex = new Texture2D(0);
	tex->_targetType = GL_TEXTURE_2D_MULTISAMPLE;
	tex->_width = w;
	tex->_height = h;

	glGenTextures(1, &tex->_htex) ERROR_CHECKED;
	glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex->_htex) ERROR_CHECKED;

	tex->_format = ColorFormat::RGBA;

	glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, 
		samples, (GLenum)tex->_format, w, h, GL_TRUE) ERROR_CHECKED;

	return tex;
}

void Texture2D::processOptions(const TextureOptions* options, const void* data)
{
	_format = options == nullptr ? ColorFormat::RGBA : options->format;
	_precision = options == nullptr ? PrecisionType::Default : options->precision;

	switch (_format)
	{
	case anip::ColorFormat::Red:
		if (_precision == PrecisionType::UnsignedByte) _internalFormat = GL_R8;
		else if (_precision == PrecisionType::Float32) _internalFormat = GL_R32F;
		else throw invalid_argument("options->precision");
		break;
	case anip::ColorFormat::RGB:
	case anip::ColorFormat::BGR:
		if (_precision == PrecisionType::UnsignedByte) _internalFormat = GL_RGB8;
		else if (_precision == PrecisionType::Float32) _internalFormat = GL_RGB32F;
		else throw invalid_argument("options->precision");
		break;
	case anip::ColorFormat::RGBA:
	case anip::ColorFormat::BGRA:
		if (_precision == PrecisionType::UnsignedByte) _internalFormat = GL_RGBA8;
		else if (_precision == PrecisionType::Float32) _internalFormat = GL_RGBA32F;
		else throw invalid_argument("options->precision");
		break;
	default:
		throw invalid_argument("options->format");
	}

	switch (_precision)
	{
	case anip::PrecisionType::UnsignedByte:
		_type = GL_UNSIGNED_BYTE;
		break;
	case anip::PrecisionType::Float32:
		_type = GL_FLOAT;
		break;
	default:
		throw invalid_argument("options->precision");
	}

	if (options == nullptr)
	{
		glTexParameteri(_targetType, 
			GL_TEXTURE_MIN_FILTER, (GLenum)TextureOptions::Filter::Default);
		glTexParameteri(_targetType,
			GL_TEXTURE_MAG_FILTER, (GLenum)TextureOptions::Filter::Default);
		glTexParameteri(_targetType,
			GL_TEXTURE_WRAP_S, (GLenum)TextureOptions::Wrapping::Default);
		glTexParameteri(_targetType,
			GL_TEXTURE_WRAP_T, (GLenum)TextureOptions::Wrapping::Default);
		stbi_set_flip_vertically_on_load(false);
	} else
	{
		glTexParameteri(_targetType, GL_TEXTURE_WRAP_S, (GLenum)(options->wrap_s));
		glTexParameteri(_targetType, GL_TEXTURE_WRAP_T, (GLenum)(options->wrap_s));
		glTexParameteri(_targetType, GL_TEXTURE_MIN_FILTER, (GLenum)(options->min_filter));
		glTexParameteri(_targetType, GL_TEXTURE_MAG_FILTER, (GLenum)(options->mag_filter));
		stbi_set_flip_vertically_on_load(options->flip_y);
	}

	glTexImage2D(GL_TEXTURE_2D, 0, _internalFormat, _width, _height,
		0, (GLenum)_format, _type, data) ERROR_CHECKED;
}

void Texture2D::setData(const void* data)
{
	glBindTexture(_targetType, _htex) ERROR_CHECKED;
	//glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexSubImage2D(_targetType, 0, 0, 0, _width, _height,
		(GLenum)_format, _type, data) ERROR_CHECKED;
}

void Texture2D::setData(int w, int h, const void* data)
{
	glBindTexture(_targetType, _htex) ERROR_CHECKED;
	//glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexImage2D(_targetType, 0, _internalFormat, w, h,
		0, (GLenum)_format, _type, data) ERROR_CHECKED;
}

bool anip::Texture2D::setPartialData(int x, int y, int w, int h, const void* data)
{
	// ANIP_FIXME: border? http://docs.gl/gl4/glTexSubImage2D
	if (x < 0 || y < 0 || x + w > _width || y + h > _height)
		return false;

	glBindTexture(_targetType, _htex) ERROR_CHECKED;
	//glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexSubImage2D(_targetType, 0, x, y, w, h,
		(GLenum)_format, _type, data) ERROR_CHECKED;
	return true;
}

void Texture2D::use(int i) const
{
	static int currentActive = GL_TEXTURE0;
	if (i >= 0 && currentActive != GL_TEXTURE0 + i)
	{
		currentActive = GL_TEXTURE0 + i;
		glActiveTexture(currentActive) ERROR_CHECKED;
	}
	glBindTexture(_targetType, _htex) ERROR_CHECKED;
}

inline int getChannels(ColorFormat format)
{
	int channels;
	switch (format)
	{
	case ColorFormat::Red:
		channels = 1; break;
	case ColorFormat::RGB:
	case ColorFormat::BGR:
		channels = 3; break;
	case ColorFormat::BGRA:
	case ColorFormat::RGBA:
		channels = 4; break;
	default:
		throw invalid_argument("Invalid ColorFormat");
	}
	return channels;
}

unsigned char* Texture2D::getData(ColorFormat format) const
{
	int channels = getChannels(format);
	unsigned char* data = new unsigned char[_width * _height * channels];
	glBindTexture(_targetType, _htex) ERROR_CHECKED;
	glGetTexImage(_targetType, 0, (GLenum)format, _type, data);
	return data;
}

unsigned char* Texture2D::getData(ColorFormat format, size_t& size) const
{
	int channels = getChannels(format);
	unsigned char* data = new unsigned char[_width * _height * channels];
	glBindTexture(_targetType, _htex) ERROR_CHECKED;
	glGetTexImage(_targetType, 0, (GLenum)format, _type, data);
	size = _width * _height * channels;
	return data;
}

void anip::Texture2D::getData(void* buf, ColorFormat format) const
{
	//glPixelStorei(GL_PACK_ALIGNMENT, 1);
	int channels = getChannels(format);
	glBindTexture(_targetType, _htex) ERROR_CHECKED;
	glGetTexImage(_targetType, 0, (GLenum)format, _type, buf);
}

//void Texture2D::saveImage(string path) const
//{
//	unsigned char* data = getData(ColorFormat::BGRA);
//	cv::Mat mat(_height, _width, CV_8UC4, data),
//		flipped(_height, _width, CV_8UC4, cv::Scalar(0, 0, 0));
//	cv::flip(mat, flipped, 0);
//	cv::imwrite(path, flipped);
//	delete data;
//}

void Texture2D::finalize()
{
	glDeleteTextures(1, &_htex);
}